import { BodyItemConstant, SimulationItemConstant as SiC } from '../constants/InputItemConstants';
import CommonConstants from '../constants/CommonConstants';

const {
  EXERCISE_TIME_SITTING,
  EXERCISE_TIME_STANDING,
  EXERCISE_TIME_LABORING,
  ALCOHOL_HABIT,
  ALCOHOL_FREQUENCY,
  ALCOHOL_AMOUNT_SAKE,
} = BodyItemConstant;

// 飲酒頻度（週換算）
export const FREQUENCY = {
  [ALCOHOL_FREQUENCY.MONTH_LESS1]: 0.0,         // 月1日未満
  [ALCOHOL_FREQUENCY.MONTH_LESS3]: 0.5,         // 月1～3日
  [ALCOHOL_FREQUENCY.WEEK_LESS2]: 1.5,          // 週1～2日
  [ALCOHOL_FREQUENCY.WEEK_LESS4]: 3.5,          // 週3～4日
  [ALCOHOL_FREQUENCY.ALMOST_EVERY_DAY]: 5.5,    // 週5～6日
  [ALCOHOL_FREQUENCY.EVERY_DAY]: 7.0,           // 毎日
};

/**
 * 計算ロジックなどで共通の処理をユーティリティ化
 */
export default class {

  /**
   * BMIを計算します。
   * @param {string|number} bodyHeight 身長（cm）
   * @param {string|number} bodyWeight 体重（kg）
   * @return {number} BMI
   */
  static calcBMI(bodyHeight, bodyWeight) {
    if (typeof (bodyHeight) === 'object') {
      let obj = bodyHeight;
      bodyHeight = obj.bodyHeight;
      bodyWeight = obj.bodyWeight;
    }

    let height = Number(bodyHeight) / 100.0;  // 身長（m）
    let weight = Number(bodyWeight);          // 体重（kg）

    if (!(height > 0) || !(weight > 0)) {
      throw new Error(`<BMI計算> 想定外のデータ: 身長=${bodyHeight}, 体重=${bodyWeight}`);
    }

    return weight / (height * height);
  }

  /**
   * 1日あたりの身体活動量（メッツ）を計算します。
   * @param {string|number} exerciseTimeSitting 運動習慣（座っている時間）
   * @param {string|number} exerciseTimeStanding 運動習慣（立ったり歩いたりしている時間）
   * @param {string|number} exerciseTimeLaboring 運動習慣（肉体労働やスポーツをする時間）
   * @return {number} メッツ
   */
  static calcMets(exerciseTimeSitting, exerciseTimeStanding, exerciseTimeLaboring) {
    if (typeof (exerciseTimeSitting) === 'object') {
      let obj = exerciseTimeSitting;
      exerciseTimeSitting = obj.exerciseTimeSitting;
      exerciseTimeStanding = obj.exerciseTimeStanding;
      exerciseTimeLaboring = obj.exerciseTimeLaboring;
    }

    const categoryA = 4.5;  // A: 4.5METs 肉体労働・激しいスポーツ
    const categoryB = 1.5;  // B: 1.5METs 座っている
    const categoryC = 2.0;  // C: 2.0METs 歩いたり立っている
    const categoryD = 0.9;  // D: 0.9METs 睡眠などその他の活動

    // 肉体労働や運動している時間
    const LABORING_TIME = {
      [EXERCISE_TIME_LABORING.NONE]: 0.0,     // なし
      [EXERCISE_TIME_LABORING.SHORT]: 0.5,    // 1時間未満
      [EXERCISE_TIME_LABORING.LONG]: 3.0,     // 1時間以上
    };
    const laboringTime = LABORING_TIME[exerciseTimeLaboring];

    // 座っている時間
    const SITTING_TIME = {
      [EXERCISE_TIME_SITTING.SHORT]: 1.5,     // 3時間未満
      [EXERCISE_TIME_SITTING.MIDDLE]: 5.5,    // 3～8時間未満
      [EXERCISE_TIME_SITTING.LONG]: 7.5,      // 8時間以上
    };
    const sittingTime = SITTING_TIME[exerciseTimeSitting];

    // 立ったり歩いたりしている時間
    const STANDING_TIME = {
      [EXERCISE_TIME_STANDING.SHORT]: 0.5,    // 1時間未満
      [EXERCISE_TIME_STANDING.MIDDLE]: 2.0,   // 1～3時間未満
      [EXERCISE_TIME_STANDING.LONG]: 8.5,     // 3時間以上
    };
    const standingTime = STANDING_TIME[exerciseTimeStanding];

    if (laboringTime === undefined || sittingTime === undefined || standingTime === undefined) {
      throw new Error(`<メッツ計算> 想定外のデータ: 座っている時間=${exerciseTimeSitting} 立ったり歩いたりしている時間=${exerciseTimeStanding} 肉体労働やスポーツをする時間=${exerciseTimeLaboring}`);
    }

    // 睡眠などその他の時間
    const otherTime = 24.0 - (laboringTime + sittingTime + standingTime);

    // メッツ計算
    const mets = (categoryA * laboringTime) + (categoryB * sittingTime) + (categoryC * standingTime) + (categoryD * otherTime);
    return mets;
  }

  /**
   * 1週間あたりのエタノール摂取量を計算します。
   * @param {string|number} alcoholHabit 飲酒習慣
   * @param {string|number} alcoholFrequency 飲酒頻度
   * @param {string|number} alcoholAmountSake 飲酒量（日本酒）
   * @param {string|number} alcoholAmountShochu 飲酒量（焼酎または泡盛）
   * @param {string|number} alcoholAmountBeer 飲酒量（ビールまたは発泡酒）
   * @param {string|number} alcoholAmountWhiskey 飲酒量（ウィスキーまたはブランデー）
   * @param {string|number} alcoholAmountWine 飲酒量（ワイン）
   * @return {number} エタノール摂取量（g）
   */
  static calcEthanol(alcoholHabit, alcoholFrequency, alcoholAmountSake, alcoholAmountShochu, alcoholAmountBeer, alcoholAmountWhiskey, alcoholAmountWine) {
    if (typeof (alcoholHabit) === 'object') {
      let obj = alcoholHabit;
      alcoholHabit = obj.alcoholHabit;
      alcoholFrequency = obj.alcoholFrequency;
      alcoholAmountSake = obj.alcoholAmountSake;
      alcoholAmountShochu = obj.alcoholAmountShochu;
      alcoholAmountBeer = obj.alcoholAmountBeer;
      alcoholAmountWhiskey = obj.alcoholAmountWhiskey;
      alcoholAmountWine = obj.alcoholAmountWine;
    }

    // 飲酒習慣＝「飲まない」の場合は、入力画面をスキップするため、エタノール摂取量は計算不能
    if (alcoholHabit === ALCOHOL_HABIT.NO_DRINKING.value) {
      return 0.0;
    }

    if (alcoholHabit === undefined || alcoholFrequency === undefined || alcoholAmountSake === undefined || alcoholAmountShochu === undefined || alcoholAmountBeer === undefined || alcoholAmountWhiskey === undefined || alcoholAmountWine === undefined) {
      throw new Error(`<エタノール摂取量計算> 想定外のデータ: アルコール飲料を飲みますか？=${alcoholHabit} 飲む頻度=${alcoholFrequency} 日本酒で=${alcoholAmountSake} 焼酎で=${alcoholAmountShochu} ビールで=${alcoholAmountBeer} ウィスキーで=${alcoholAmountWhiskey} ワインで=${alcoholAmountWine}`);
    }

    const totalEthanol = this.calcUnitEthanol(alcoholHabit, alcoholAmountSake, alcoholAmountShochu, alcoholAmountBeer, alcoholAmountWhiskey, alcoholAmountWine);

    const frequency = FREQUENCY[alcoholFrequency];

    if (frequency === undefined) {
      throw new Error(`<エタノール摂取量計算> 想定外のデータ: 飲酒頻度=${frequency}`);
    }

    // 週あたりのエタノール総量（g）
    const ethanolPerWeek = totalEthanol * frequency;
    return ethanolPerWeek;
  }

  /**
   * 各種酒類の合計のエタノール摂取量を計算します。
   * @param {string|number} alcoholHabit 飲酒習慣
   * @param {string|number} alcoholAmountSake 飲酒量（日本酒）
   * @param {string|number} alcoholAmountShochu 飲酒量（焼酎または泡盛）
   * @param {string|number} alcoholAmountBeer 飲酒量（ビールまたは発泡酒）
   * @param {string|number} alcoholAmountWhiskey 飲酒量（ウィスキーまたはブランデー）
   * @param {string|number} alcoholAmountWine 飲酒量（ワイン）
   * @return {number} エタノール摂取量（g）
   */
  static calcUnitEthanol(alcoholHabit, alcoholAmountSake, alcoholAmountShochu, alcoholAmountBeer, alcoholAmountWhiskey, alcoholAmountWine) {

    if (alcoholHabit === undefined || alcoholAmountSake === undefined || alcoholAmountShochu === undefined || alcoholAmountBeer === undefined || alcoholAmountWhiskey === undefined || alcoholAmountWine === undefined) {
      throw new Error(`<エタノール摂取量計算> 想定外のデータ: アルコール飲料を飲みますか？=${alcoholHabit} 日本酒で=${alcoholAmountSake} 焼酎で=${alcoholAmountShochu} ビールで=${alcoholAmountBeer} ウィスキーで=${alcoholAmountWhiskey} ワインで=${alcoholAmountWine}`);
    }

    // 1回あたりの飲酒量（係数）※日本酒で代表値を定義※
    const AMOUNT = {
      [ALCOHOL_AMOUNT_SAKE.NO_DRINKING]: 0.0,   // 飲まない
      [ALCOHOL_AMOUNT_SAKE.ALMOST_NOT]: 0.5,    // 0.5合未満
      [ALCOHOL_AMOUNT_SAKE.LESS1]: 0.75,        // 0.5～1合未満
      [ALCOHOL_AMOUNT_SAKE.EQUAL1]: 1.0,        // 1合
      [ALCOHOL_AMOUNT_SAKE.EQUAL2]: 2.0,        // 2合
      [ALCOHOL_AMOUNT_SAKE.EQUAL3]: 3.0,        // 3合
      [ALCOHOL_AMOUNT_SAKE.EQUAL4]: 4.0,        // 4合
      [ALCOHOL_AMOUNT_SAKE.LESS6]: 5.0,         // 5～6合
      [ALCOHOL_AMOUNT_SAKE.MORE7]: 7.0,         // 7合以上
    };
    const sakeAmount = AMOUNT[alcoholAmountSake];         // 日本酒
    const shochuAmount = AMOUNT[alcoholAmountShochu];     // 焼酎または泡盛
    const beerAmount = AMOUNT[alcoholAmountBeer];         // ビールまたは発泡酒
    const whiskeyAmount = AMOUNT[alcoholAmountWhiskey];   // ウィスキーまたはブランデー
    const wineAmount = AMOUNT[alcoholAmountWine];         // ワイン

    if (sakeAmount === undefined || shochuAmount === undefined || beerAmount === undefined || whiskeyAmount === undefined || wineAmount === undefined) {
      throw new Error(`<エタノール摂取量計算> 想定外のデータ: 日本酒=${sakeAmount} 焼酎=${shochuAmount} ビール=${beerAmount} ウィスキー=${whiskeyAmount} ワイン=${wineAmount}`);
    }

    // 1回あたりのエタノール摂取量（g）
    const sakeEthanol = sakeAmount * 23.0;          // 日本酒
    const shochuEthanol = shochuAmount * 36.0;      // 焼酎または泡盛
    const beerEthanol = beerAmount * 23.0;          // ビールまたは発泡酒
    const whiskeyEthanol = whiskeyAmount * 10.0;    // ウィスキーまたはブランデー
    const wineEthanol = wineAmount * 10.0;          // ワイン
    const totalEthanol = sakeEthanol + shochuEthanol + beerEthanol + whiskeyEthanol + wineEthanol;

    return totalEthanol;
  }

  /**
   * オブジェクトのプロパティをすべて数値に変換しコピーします。
   * ※ただし、未入力（undefined, null, ''）と見做せるプロパティは undefined に変換します。
   * @param {Object} 任意のオブジェクト
   * @return 数値変換後のオブジェクト
   */
  static copyAndString2Number(data) {
    let r = {};
    for (let key in data) {
      const value = data[key];
      r[key] = (value === undefined || value === null || value === '') ? undefined : Number(value);
    }
    return r;
  }

  /**
   * 数値（小数点第n位）取得（小数点第n+1位以下を四捨五入）
   * @param {number} numericalValue 数値
   * @param {number} place 小数点第n位
   * @return {number} 数値（小数点第n位）
   */
  static getFloatFormat(numericalValue, place) {
    let pow = Math.pow(10, place);
    return Math.round(numericalValue * pow) / pow;
  }

  /**
   * riskLevel(HIGH(高い), LOW(低い)などの区分)を追加して返却
   * @param {object} results calcRiskの戻り値(you:リスク計算結果, min, max...)
   * @return {object} results riskLevelを追加したresults
   */
  static diagnosisRiskLevel(results) {
    const { you, min, max } = results.probability;

    let riskLevel = '';
    const pow = Math.pow(10, 3);
    const range = Math.ceil(Math.floor(((max - min) / 3) * pow * 10) / 10) / pow;
    const highBorder = (max * pow - range * pow) / pow;
    const lowBorder = (max * pow - (range * pow) * 2) / pow;
    
    if (you >= highBorder) {
      riskLevel = 'HIGH';
    } else if (you < highBorder && you >= lowBorder) {
      riskLevel = 'A_LITTLE_HIGH';
    } else {
      riskLevel = 'LOW';
    }

    results.probability.riskLevel = riskLevel;
    return results;
  }

  // デフォルト値返却（undefined, null, NaN）
  static ifNaN(value, defaultValue) {
    return (value === undefined || value === null || isNaN(value)) ? defaultValue : value;
  }

  // デフォルト値返却（undefined, null, 空）
  static ifEmpty(value, defaultValue) {
    return (value === undefined || value === null || value.length === 0) ? defaultValue : value;
  }

  // デフォルト値返却（undefined, null）
  static ifNull(value, defaultValue) {
    return (value === undefined || value === null) ? defaultValue : value;
  }

  // 69歳を超えた場合、69歳とみなす
  static convertAge(age) {
    if (!(age > -1)) throw new Error(`<70歳以上変換> 想定外のデータ: 年齢=${age}`);
    return age > 69 ? 69 : age;
  }

  /**
   * 身長とBMIから体重を逆算します。
   * @param {string|number} bodyHeight 身長（cm）
   * @param {string|number} bmiVal BMI
   * @return {number} 体重（kg）
   */
  static calcWeightFromBMI(bodyHeight, bmiVal) {
    if (typeof (bodyHeight) === 'object') {
      let obj = bodyHeight;
      bodyHeight = obj.bodyHeight;
      bmiVal = obj.bmiVal;
    }

    let height = Number(bodyHeight) / 100.0;  // 身長（m）
    let bmi = Number(bmiVal);          // BMI

    if (!(height > 0) || !(bmi > 0)) {
      throw new Error(`<体重計算> 想定外のデータ: 身長=${bodyHeight}, BMI=${bmiVal}`);
    }

    return bmi * (height * height);
  }

  /**
   * 循環器系リスク用の健康なグループと不健康なグループの項目ごとのラベルと値のセットを返す
   */
  static getHealthyCirculatoryDiseases({ gender, age, bodyHeight }, dispWeight = true, dispCholesterol = true) {
    // 69歳を超えた場合、69歳とみなす
    age = this.convertAge(age);
    const genderValue = gender === SiC.GENDER.MALE.value ? 1 : 0;
    const SMOKING_LABEL = '喫煙';
    const DIABETES_LABEL = '糖尿病';
    const BLOOD_SUGAR_LEVEL_FASTING_LABEL = '<div><p>血糖値</p><p>空腹時</p></div>';
    const BLOOD_PRESSURE_LABEL = '血圧';
    const CHOLESTEROL_LABEL = 'コレステロール';
    const HDL_CHOLESTEROL_LABEL = '<div><p>HDL</p><p>コレステロール</p></div>';

    // min, max用 血圧、HDL、NonHDLの値
    const { minBP, minHdl, maxBP, maxHdl} = this.calcMaxMinMidParamCirculatory(genderValue, age);
    const { goodWeight, badWeight } = this._getWeightGoodBadCirculatory(bodyHeight);

    // 健康なグループ、不健康なグループ配列
    let good = [];
    let bad = [];
    if (dispWeight) {
      good.push(
        {
          label: SiC.BODY_WEIGHT.label,
          value: `${goodWeight.toFixed(CommonConstants.DECIMAL_DIGIT)}kg以下`,
          addCss: '',
        }
      );
      bad.push(
        {
          label: SiC.BODY_WEIGHT.label,
          value: `${badWeight.toFixed(CommonConstants.DECIMAL_DIGIT)}kg以上`,
          addCss: '',
        }
      );
    }

    let goodMaxBP = '100';
    let badMaxBP = '180';
    if (dispCholesterol) {
      goodMaxBP = minBP;
      badMaxBP = maxBP;
    }

    good.push(
      { label: SMOKING_LABEL, value: SiC.SMOKING_HABIT.NO_SMOKING.label, addCss: '' },
      { label: DIABETES_LABEL, value: SiC.DIABETES_CURE_FLG.NO.label, addCss: '' },
      { label: BLOOD_SUGAR_LEVEL_FASTING_LABEL, value: SiC.BLOOD_SUGAR_LEVEL_FASTING.LOW.label, addCss: '' },
      { label: SiC.ANTIHYPERTENSIVE_FLG.label, value: SiC.ANTIHYPERTENSIVE_FLG.NO.label, addCss: '' },
      { label: BLOOD_PRESSURE_LABEL, value: `${goodMaxBP}mmHg～70mmHg`, addCss: '' },
    );
    bad.push(
      { label: SMOKING_LABEL, value: SiC.SMOKING_HABIT.IS_SMOKING.label, addCss: '' },
      { label: DIABETES_LABEL, value: SiC.DIABETES_CURE_FLG.YES.label, addCss: '' },
      { label: BLOOD_SUGAR_LEVEL_FASTING_LABEL, value: SiC.BLOOD_SUGAR_LEVEL_FASTING.HIGH.label, addCss: '' },
      { label: SiC.ANTIHYPERTENSIVE_FLG.label, value: SiC.ANTIHYPERTENSIVE_FLG.YES.label, addCss: '' },
      { label: BLOOD_PRESSURE_LABEL, value: `${badMaxBP}mmHg～110mmHg`, addCss: '' },
    );

    if (dispCholesterol) {
      good.push(
        { label: CHOLESTEROL_LABEL, value: '', addCss: ' title' },
        { label: HDL_CHOLESTEROL_LABEL, value: `${minHdl}mg/dl`, addCss: ' wide-label' },
      );
      bad.push(
        { label: CHOLESTEROL_LABEL, value: '', addCss: ' title' },
        { label: HDL_CHOLESTEROL_LABEL, value: `${maxHdl}mg/dl`, addCss: ' wide-label' },
      );
    }

    return { good, bad };
  }

  /**
   * 循環器系リスク用に、身長から健康なグループの体重範囲と不健康なグループの体重範囲を取得
   * @param {string|number} bodyHeight 身長（cm）
   * @return {number} BMI
   */
  static _getWeightGoodBadCirculatory(bodyHeight) {
    const GOOD_BMI = 20.0;
    const BAD_BMI = 35.0;
    return {
      goodWeight: this.calcWeightFromBMI(bodyHeight, GOOD_BMI),
      badWeight: this.calcWeightFromBMI(bodyHeight, BAD_BMI),
    };
  }

  /**
   * 循環器系リスク用に、性別および年代別の min, max用 血圧、HDL、NonHDLの値を取得
   * @param {number} genderValue 性別
   * @param {number} age         年齢
   * @returns min, max用中間値(血圧、HDLコレステロール、non-HDL値)
   */
  static calcMaxMinMidParamCirculatory(genderValue, age) {
    // 1次元  0:女性、1:男性
    // 2次元  0:50才未満、1:50才以上,60才未満、2:60才以上
    const DEF_MIN_BP = [[95, 100, 95], [100, 100, 100]]; // 血圧
    const DEF_MIN_HDL = [[95, 90, 90], [90, 90, 90]];    // HDL
    const DEF_MIN_NONHDL = [[80, 90, 90], [75, 80, 75]]; // NonHdl

    const DEF_MAX_BP = [[160, 170, 180], [170, 175, 180]];
    const DEF_MAX_HDL = [[35, 35, 35], [30, 30, 30]];
    const DEF_MAX_NONHDL = [[210, 230, 230], [240, 220, 220]];

    const ageIndex = this._getAgeIndex(age);

    return {
      minBP: DEF_MIN_BP[genderValue][ageIndex],
      minHdl: DEF_MIN_HDL[genderValue][ageIndex],
      minNonHdl: DEF_MIN_NONHDL[genderValue][ageIndex],
      maxBP: DEF_MAX_BP[genderValue][ageIndex],
      maxHdl: DEF_MAX_HDL[genderValue][ageIndex],
      maxNonHdl: DEF_MAX_NONHDL[genderValue][ageIndex],
    };
  }

  /**
   * 属する世代のコード値を取得
   * @param {number} age 年齢
   * @returns 0:50才未満、1:50才以上,60才未満、2:60才以上
   */
  static _getAgeIndex(age) {
    let ageIndex;
    if (age < 50) {
      ageIndex = 0;
    } else if (age < 60) {
      ageIndex = 1;
    } else {
      ageIndex = 2;
    }
    return ageIndex;
  }

}
